feat(security): split managedBy:'system' into engine-owned vs admin-writable, guard engine-owned writes (#3220)#3315
Merged
Conversation
…ritable, guard engine-owned writes (#3220) The `system` bucket conflated two incompatible write policies — engine-owned rows (never user-written) and platform-schema, admin/user-writable data — under one all-false affordance row with no engine enforcement (only better-auth had a write guard). This left a wildcard admin able to raw-write engine-owned rows through the generic data API (ADR-0049 gap). Rather than add a new managedBy enum value (which falls through to fully-editable platform defaults on deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and "engine-owned" is defined as a system/append-only object granting no write: - Writable set declares userActions{create,edit,delete}: the RBAC link tables, sys_user_preference, sys_approval_delegation, and the messaging config grids (notification preference/subscription/template). Affordance only — the DelegatedAdminGate / RLS / permission sets remain the authz. - Engine-owned objects locked to apiMethods ['get','list'] where absent (jobs, notifications, approvals, record-share, automation-run, mail/settings/secret audit, messaging delivery pipeline). sys_secret is read-locked explicitly (an empty apiMethods array fails open). - sys_import_job stays engine-owned: the REST import route writes job rows isSystem-elevated (attribution preserved via explicit created_by). - New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes keyed off resolved affordances; isSystem/context-less writes bypass. Wired into the security middleware alongside the other data-layer gates. - reconcileManagedApiMethods (objectql registry) now runs for every managed bucket, stripping advertised write verbs the affordances forbid. - /me/permissions clamp (plugin-hono-server) now clamps system/append-only too. ADR-0103. Refs #3220, follows #3222 / ADR-0049 / ADR-0092. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 10 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 19, 2026 18:04
This was referenced Jul 20, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the root cause tracked in #3220 (follow-through on #3222 / #1591 / #3213). ADR-0103.
Problem
managedBy: 'system'conflated two incompatible write policies:isSystem/ a serviceSYSTEM_CTX/ a context-less engine call (jobs, notifications, approvals, sharing, automation runs, the messaging delivery pipeline, settings/secrets/audit).DelegatedAdminGate-governed),sys_user_preference,sys_approval_delegation, and the messaging config grids surfaced in Setup.The bucket carried the same all-false affordance row as
better-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write engine-owned rows through the generic data API (ADR-0049 gap). #3222 could only patch two latent holes; this is the taxonomy fix.Approach — resolved affordance is the policy (no new enum value)
A new
managedByvalue would fall through to the fully-editableplatformdefault on already-deployed Console clients. Instead, write policy isresolveCrudAffordances(bucket default +userActions), and engine-owned ≔ asystem/append-onlyobject granting no write. Guard,apiMethodsreconciliation, and the/me/permissionsclamp all key off that one contract.What's in the diff
userActions{create,edit,delete}—sys_user_position,sys_user_permission_set,sys_position_permission_set,sys_user_preference,sys_approval_delegation,sys_notification_preference/_subscription/_template. Affordance only —DelegatedAdminGate/ RLS / permission sets stay the authz.apiMethods: ['get','list']where absent.sys_secretread-locked explicitly (an emptyapiMethodsarray fails open).sys_import_jobstays engine-owned — the REST import route writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp).assertEngineOwnedWriteAllowed(plugin-security) — fail-closed on user-context generic writes to engine-owned objects;isSystem/ context-less writes bypass. Wired into the security middleware alongside the other data-layer gates.reconcileManagedApiMethodsnow runs for every managed bucket (wasbetter-auth-only) — the drift backstop.clampManagedObjectWritesnow clampssystem/append-onlytoo.Audit finding (write-context, the guard's precondition)
Confirmed every engine-owned writer uses
isSystem/SYSTEM_CTX/ a context-less engine call (incl. the metadata-protocol repository, whose transaction context carries nouserId). The audit expanded the writable set from 5 to 8:notification_preference/_subscription/_templateare Setup nav grids users/admins author, so they joined the writable set rather than being locked down.managed_byprovenance vocabulary (platform/package/admin, ADR-0066) — a different axis, untouched.Verification
All green:
spec(6795) ·objectql(1023, incl. new reconcile cases) ·plugin-security(521, incl. newsystem-write-guardsuite) ·plugin-authidentity-write-guard (15) ·plugin-hono-server(92, updated fold/clamp) ·rest(323) ·platform-objects/plugin-approvals/service-messaging/service-automation/plugin-sharing/metadata-core.delegation-of-duty+showcase-permission-zoo(18) — confirm delegated administration and direct permission-set grants under user context still work with the guard in place.Potentially breaking
A downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open will have those verbs stripped (with a warning) and user-context generic writes rejected — declareuserActionsopening the verbs it legitimately takes.better-authkeeps plugin-auth's identity guard unchanged.The Console copy refinements (badge/empty-state for the writable-system case) ship in the paired objectui PR; they are not required for correctness (the UI already honours
userActions).🤖 Generated with Claude Code
Generated by Claude Code